ENTSO-E Electricity Data Analysis¶

Imports¶

In [1]:
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
import sys
sys.path.insert(1,'../')
from scripts.plotting_utils import *

Load Processed Data¶

In [2]:
df = pd.read_feather("../data/processed_data.feather")
df.DE_LU.demand
Out[2]:
2023-12-31 23:00:00+00:00    39981.75
2024-01-01 00:00:00+00:00    38085.75
2024-01-01 01:00:00+00:00    37063.50
2024-01-01 02:00:00+00:00    36834.25
2024-01-01 03:00:00+00:00    36835.25
                               ...   
2025-08-18 18:00:00+00:00    54441.00
2025-08-18 19:00:00+00:00    52577.75
2025-08-18 20:00:00+00:00    49661.00
2025-08-18 21:00:00+00:00    46344.50
2025-08-18 22:00:00+00:00    46344.50
Name: demand, Length: 14304, dtype: float64
In [3]:
df.DE_LU.index
Out[3]:
DatetimeIndex(['2023-12-31 23:00:00+00:00', '2024-01-01 00:00:00+00:00',
               '2024-01-01 01:00:00+00:00', '2024-01-01 02:00:00+00:00',
               '2024-01-01 03:00:00+00:00', '2024-01-01 04:00:00+00:00',
               '2024-01-01 05:00:00+00:00', '2024-01-01 06:00:00+00:00',
               '2024-01-01 07:00:00+00:00', '2024-01-01 08:00:00+00:00',
               ...
               '2025-08-18 13:00:00+00:00', '2025-08-18 14:00:00+00:00',
               '2025-08-18 15:00:00+00:00', '2025-08-18 16:00:00+00:00',
               '2025-08-18 17:00:00+00:00', '2025-08-18 18:00:00+00:00',
               '2025-08-18 19:00:00+00:00', '2025-08-18 20:00:00+00:00',
               '2025-08-18 21:00:00+00:00', '2025-08-18 22:00:00+00:00'],
              dtype='datetime64[ns, UTC]', length=14304, freq=None)

Analysis and Visualization¶

In [4]:
start = pd.Timestamp('20240630', tz='Europe/Brussels')
end = pd.Timestamp('20250630', tz='Europe/Brussels')
period = slice(start,end)

# df_FR = df.FR.loc[period]
df_FR = df.FR
data = []
data.append(
    go.Scatter(
        x=df_FR.index,
        y=df_FR.demand,
        line=dict(
            width=0.5,
            color=light_blue_gray,
        ),
        showlegend=False,
    )
)
data.append(
    go.Scatter(
        x=df_FR.index,
        y=df_FR.demand.rolling(window=24 * 7*4, center=True, min_periods=24*7).mean(),
        line=dict(
            color=fca_blue,
        ),
        name="1 month moving average",
    )
)
In [5]:
layout = go.Layout(
    template=fca_template,
    title_text="Electricity consumption France (MW)",
    yaxis_range=[0,89e3]
)
fig = go.Figure(data=data, layout=layout)
fig.show()
In [6]:
layout = go.Layout(
    template=fca_template,
    title_text="Electricity consumption over one week, France (MW)",
    yaxis_range=[0,89e3],
    xaxis_range=[
        pd.Timestamp('2023-08-18 22:00:00'),
        pd.Timestamp('2023-08-26 4:00:00')
    ]
)
fig = go.Figure(data=data, layout=layout)
fig.show()
In [7]:
px.density_heatmap(
    df_FR,
    x="hour",
    y="demand",
    # y="weighted_market_val",
    nbinsy=60,
    nbinsx=24,
    color_continuous_scale=cm_data_plotly,
).update_layout(template=fca_template, title_text='Frequency of demand by hour, France (green-yellow = most frequent values)')
In [8]:
df_DE = df.DE_LU

# 0.109 is the capital recovery factor

avgs = []
for q in np.linspace(0,1,500):
    quant = df_DE.price.quantile(q)
    mean = df_DE.query('price <= @quant').price.mean()
    # avgs.append(mean if mean >= 0 else 0)
    avgs.append(mean)
avgs = np.array(avgs)
fig = go.Figure()
fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=avgs / 1.0, # 100% efficiency, roughly cancels out with gas price being GCV ~50/55?
        name="average Electricity cost 1MWh_heat",
        line_color="black",
    )
)
fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=1000e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
        name="1000 EUR/kW",
        line_color=blue_gray,
        line_dash="dot",
    )
)
# fig.add_trace(go.Scatter(x=np.linspace(0,1,100), y=500e3*0.109/(365*24*np.linspace(0,1,100)), name='500 EUR/kW' ))
fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=500e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
        name="500 EUR/kW",
        line_color=dark_gray,
        line_dash="dot",
    )
)
fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=250e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
        name="250 EUR/kW",
        line_color=fca_blue,
        line_dash="dot",
    )
)
fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=100e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
        name="100 EUR/kW",
        line_color=highlight_blue,
        line_dash="dot",
    )
)
fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=avgs 
        + 1000e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
        showlegend=False,
        line_color=blue_gray,
    )
)

fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=avgs 
        + 500e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
        name="LCoHeat",
        line_color=dark_gray,
    )
)
fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=avgs 
        + 250e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
        name="LCoHeat",
        line_color=fca_blue,
    )
)
fig.add_trace(
    go.Scatter(
        x=np.linspace(0, 1, 500),
        y=avgs 
        + 100e3 * 0.109 / (365 * 24 * np.linspace(0, 1, 500)),
        showlegend=False,
        line_color=highlight_blue,
    )
)
# fig.add_trace(go.Scatter(x=np.linspace(0,1,500), y=df_DE.price.quantile(np.linspace(0,1,500)) + 500e3*0.109/(365*24*np.linspace(0,1,500)),name='LCoH'))
fig.update_layout(
    dict(
        width=800,
        title="LCOHeat for Germany, 01/2023 - 12/2023<br> as a function of production at cheapest X percent of price hours",
        xaxis_title="price hour percentile",
        yaxis_title="eur/MWh",
    )
)
fig.update_layout(template=fca_template, yaxis_range=[-25,110], xaxis_range=[0,1.19], legend=dict(x=1, y=0.6))
fig.show()
/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:24: RuntimeWarning:

divide by zero encountered in divide

/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:34: RuntimeWarning:

divide by zero encountered in divide

/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:43: RuntimeWarning:

divide by zero encountered in divide

/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:52: RuntimeWarning:

divide by zero encountered in divide

/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:62: RuntimeWarning:

divide by zero encountered in divide

/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:72: RuntimeWarning:

divide by zero encountered in divide

/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:81: RuntimeWarning:

divide by zero encountered in divide

/var/folders/3q/5mkhpnj53p1gc9gp2z3f4tkm0000gn/T/ipykernel_62257/2248199203.py:90: RuntimeWarning:

divide by zero encountered in divide

fix me¶

In [9]:
df_EU = df
# # only 202X
y0 = 2022
y1 = 2023
# period = slice(f'{y0}-01-01 00:00:00',f'{y1}-01-01 00:00:00')
df_DE_2 = df_EU.DE_LU.copy()

y0, y1 = 2023, 2024
df23 = (df_DE_2
    .copy()
    .loc[slice(f'{y0}-01-01 00:00:00', f'{y1}-01-01 00:00:00')]
    .assign(market_val = lambda df: df.price * df.vre / df.demand)
    .assign(vre_quant = lambda df: df.vre/df.vre.max())
)
vre_mean = df23.vre.mean()
vre_quant = df23.vre.quantile(0.75)
bin_width = 1000
df_DE_2 = df23.copy()

df_DE_2_vre = df_DE_2.assign(vre_bin=lambda df: np.floor((df.vre / bin_width))*bin_width).groupby('vre_bin').price.describe()
df_DE_2_means = df_DE_2.assign(vre_bin=lambda df: np.floor((df.vre / bin_width))*bin_width).groupby('vre_bin').mean()
df_DE_2_median = df_DE_2.assign(vre_bin=lambda df: np.floor((df.vre / bin_width))*bin_width).groupby('vre_bin').median()
# df_DE_2_wind = df_DE_2.assign(wind_bin=lambda df: np.floor((df.wind / bin_width))*bin_width).groupby('wind_bin').mean()
df_DE_2_wind = df_DE_2.assign(wind_bin=lambda df: np.floor((df.wind / bin_width))*bin_width).groupby('wind_bin').price.describe()
# df_DE_2_wind['mean']
df_DE_2_wind.head()
# df_DE_2_solar = df_DE_2.assign(solar_bin=lambda df: np.floor((df.solar / bin_width))*bin_width).groupby('solar_bin').mean()
df_DE_2_solar = df_DE_2.assign(solar_bin=lambda df: np.floor((df.solar / bin_width))*bin_width).groupby('solar_bin').price.describe()
# df_DE_2_solar['mean']
df_DE_2_solar['25%']

fig = go.Figure()

scatter = go.Scatter(
    x=df_DE_2.vre,
    y=df_DE_2.market_val,
    mode='markers',
    marker=dict(opacity=0.1, size=3, color=gray),
    name='Hourly market value (VRE production × day ahead price)',
    # y="weighted_market_val",
    # nbinsy=3000,
    # nbinsx=1000,
    # color_continuous_scale="magma",
)
fig.update_layout(
    template=fca_template,
    yaxis_range=[-0.34e6, 4.2e6],
    xaxis_range=[-1000, 62e3],
    yaxis_title_text='Market value (€)',
    xaxis_title_text='VRE production (MW)',
    title=f"Market value of VREs declining with rising share of generation in Germany from {y0} to {y1}",
    legend=dict(
        x=1.1,
        y=1,
        xanchor='left',
        yanchor='top',
    ),
    height=540*1.4,
    width=960*1.4,
)
# fig.add_trace(scatter)
fig.add_trace(
    go.Scatter(
        x=[0, df_DE_2.vre.max()],
        y=[0, df_DE_2.price.mean() * df_DE_2.vre.max()],
        mode="lines",
        line_color=very_dark_gray,
        line_width=4,
        name=f'VRE production × Mean price {y0}-{y1}'
    )
)
fig.add_vline(x=df_DE_2.demand.mean(), annotation=dict(text='mean demand'))

green = 'rgba(145, 192, 150, 1)'
fig.add_trace(
    go.Scatter(
        x=df_DE_2_means.index,
        y=df_DE_2_means.price*df_DE_2_means.index,
        mode="lines",
        line_color=green,
        line_width=4,
        name='Mean VRE market value'
    )
)


green = 'rgba(145, 192, 150, 0.2)'
fig.add_traces(
    go.Scatter(
        x=df_DE_2_vre["25%"].index,
        y=df_DE_2_vre["25%"] * df_DE_2_vre["25%"].index,
        line=dict(color=green),
        name='IQR',
    )
)
fig.add_traces(
    go.Scatter(
        x=df_DE_2_vre["75%"].index,
        y=df_DE_2_vre["75%"] * df_DE_2_vre["75%"].index,
        fill="tonexty",
        fillcolor=green,
        line=dict(color=green),
        showlegend=False,
    )
)

sand_yellow = 'rgba(226, 182, 129, 1)'
fig.add_trace(
    go.Scatter(
        x=df_DE_2_solar.index,
        y=df_DE_2_solar['mean']*df_DE_2_solar.index,
        mode="lines",
        line_color=sand_yellow,
        line_width=4,
        name='Mean solar market value'
    )
)
sand_yellow = 'rgba(226, 182, 129, 0.2)'
fig.add_traces(
    go.Scatter(
        x=df_DE_2_solar["25%"].index,
        y=df_DE_2_solar["25%"] * df_DE_2_solar["25%"].index,
        line=dict(color=sand_yellow),
        name='IQR',
    )
)
fig.add_traces(
    go.Scatter(
        x=df_DE_2_solar["75%"].index,
        y=df_DE_2_solar["75%"] * df_DE_2_solar["75%"].index,
        fill="tonexty",
        fillcolor=sand_yellow,
        line=dict(color=sand_yellow),
        showlegend=False,
    )
)


highlight_blue = 'rgba(2, 147, 210, 1)'
fig.add_trace(
    go.Scatter(
        x=df_DE_2_wind.index,
        y=df_DE_2_wind['mean']*df_DE_2_wind.index,
        mode="lines",
        line_color=highlight_blue,
        line_width=4,
        name='Mean wind market value'
    )
)
highlight_blue = 'rgba(2, 147, 210, 0.2)'
fig.add_traces(
    go.Scatter(
        x=df_DE_2_wind["25%"].index,
        y=df_DE_2_wind["25%"] * df_DE_2_wind["25%"].index,
        line=dict(color=highlight_blue),
        name='IQR',
    )
)
fig.add_traces(
    go.Scatter(
        x=df_DE_2_wind["75%"].index,
        y=df_DE_2_wind["75%"] * df_DE_2_wind["75%"].index,
        fill="tonexty",
        fillcolor=highlight_blue,
        opacity=0.1,
        line=dict(color="rgba(0,0,0,0)"),
        showlegend=False,
    )
)

fig.show()
In [ ]: